﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Supermodel.DomainModel;
using System.Reflection;
using Supermodel.Exceptions;
using Supermodel.MVC.FormModels.Metadata;
using Supermodel.Reflection;
using Supermodel.MVC.Extensions;

namespace Supermodel.MVC.FormModels
{
    abstract public class FormModelForChildEntityBase<EntityT, ParentEntityT> : FormModelForEntityBase<EntityT>
        where EntityT : ChildEntity<EntityT, ParentEntityT>, new()
        where ParentEntityT : Entity<ParentEntityT>, new()
    {
        #region Form Model to/from DomainModel Mappers
        public override FormModelForEntityBase<EntityT> InitFromDomainModelEntity(EntityT domainModel)
        {
            base.InitFromDomainModelEntity(domainModel);
            ParentId = domainModel.ParentId;
            return this;
        }

        public override EntityT UpdateDomainModelEntity(EntityT domainModel)
        {
            base.UpdateDomainModelEntity(domainModel);
            domainModel.ParentId = ParentId;
            return domainModel;
        }
        #endregion

        [ScaffoldColumn(false)]
        public virtual long? ParentId { get; private set; }
    }
    
    abstract public class FormModelForEntityBase<EntityT> : FormModelBase, IValidatableObject where EntityT : Entity<EntityT>, new()
    {
        #region Form Model to/from DomainModel Mappers
        public virtual FormModelForEntityBase<EntityT> InitFromDomainModelEntity(EntityT domainModel)
        {
            foreach (var property in GetType().GetProperties())
            {
                //If property is marked NotReflectionMappedAttribute, don't worry about this one
                if (property.GetCustomAttributes(typeof(NotReflectionMappedAttribute), true).Count() == 1) continue;

                //Find matching properties
                PropertyInfo myProperty = property;
                PropertyInfo domainModelProperty = GetMatchingDomainProperty(myProperty, domainModel);


                //Now let's make sure that the type either matches or is one that we autohandle, and assign value from domain to form model

                //This is the default case of when the types match exactly
                if (myProperty.PropertyType == domainModelProperty.PropertyType)
                {
                    this.PropertySet(myProperty.Name, domainModel.PropertyGet(domainModelProperty.Name), true);
                }

                //Custom Entity Mappers
                else if (myProperty.PropertyType.GetInterface(typeof(IFormModelEntityMapper).Name) != null)
                {
                    try
                    {
                        object formModelObject;
                        if (!myProperty.PropertyType.IsGenericType) formModelObject = ReflectionHelpers.CreateType(myProperty.PropertyType);
                        else formModelObject = ReflectionHelpers.CreateGenericType(myProperty.PropertyType.GetGenericTypeDefinition(), myProperty.PropertyType.GetGenericArguments()[0]);

                        // ReSharper disable PossibleNullReferenceException
                        (formModelObject as IFormModelEntityMapper).MapFromEntityObject(domainModel.PropertyGet(domainModelProperty.Name), domainModelProperty.PropertyType);
                        // ReSharper restore PossibleNullReferenceException

                        this.PropertySet(myProperty.Name, formModelObject);
                    }
                    catch(Exception ex)
                    {
                        throw new FormModelPropertyCantBeAutomapedException(string.Format("Property '{0}' of form model class '{1}' can't be automapped to domain model type '{2}' property '{3}' because IFormModelEntityMapper threw an exception: {4}.", myProperty.Name, GetType().Name, domainModel.GetType().Name, domainModelProperty.Name, ex.Message));
                    }
                }
                    
                //If all fails
                else
                {
                    throw new FormModelPropertyCantBeAutomapedException(string.Format("Property '{0}' of form model class '{1}' can't be automapped to domain model type '{2}' property '{3}' because their types are incompatible.", myProperty.Name, GetType().Name, domainModel.GetType().Name, domainModelProperty.Name));
                }
            }
            
            // ReSharper disable RedundantThisQualifier
            this.Id = domainModel.Id;
            //this.ParentId = domainModel.ParentId;
            this.Label = domainModel.Label;
            // ReSharper restore RedundantThisQualifier

            return this;
        }
        public virtual EntityT UpdateDomainModelEntity(EntityT domainModel)
        {
            foreach (var property in GetType().GetProperties())
            {
                //If property is marked NotReflectionMappedAttribute, don't worry about this one
                if (property.GetCustomAttributes(typeof(NotReflectionMappedAttribute), true).Count() == 1) continue;

                //Find matching properties
                PropertyInfo myProperty = property;
                PropertyInfo domainModelProperty = GetMatchingDomainProperty(myProperty, domainModel);

                //Now let's make sure that the type either matches or is one that we autohandle, and assign value from domain to form model

                //This is the default case of when the types match exactly
                if (myProperty.PropertyType == domainModelProperty.PropertyType)
                {
                    domainModel.PropertySet(domainModelProperty.Name, this.PropertyGet(myProperty.Name), true);
                }

                //Custom Entity Mappers
                else if (myProperty.PropertyType.GetInterface(typeof(IFormModelEntityMapper).Name) != null)  //.GetInterfaces().Where(x => x.Name == "ICustomEntityMapper").Count() != 0)
                {
                    try
                    {
                        // ReSharper disable PossibleNullReferenceException
                        var value = (this.PropertyGet(myProperty.Name) as IFormModelEntityMapper).MapToEntityObject(domainModel.PropertyGet(domainModelProperty.Name), domainModelProperty.PropertyType);
                        // ReSharper restore PossibleNullReferenceException
                        domainModel.PropertySet(domainModelProperty.Name, value);
                    }
                    catch (Exception ex)
                    {
                        throw new FormModelPropertyCantBeAutomapedException(string.Format("Property '{0}' of form model class '{1}' can't be automapped to domain model type '{2}' property '{3}' because IFormModelEntityMapper threw an exception: {4}.", myProperty.Name, GetType().Name, domainModel.GetType().Name, domainModelProperty.Name, ex.Message));
                    }
                }

                //If all fails
                else
                {
                    throw new FormModelPropertyCantBeAutomapedException(string.Format("Property '{0}' of form model class '{1}' can't be automapped to domain model type '{2}' property '{3}' because their types are incompatible.", myProperty.Name, GetType().Name, domainModel.GetType().Name, domainModelProperty.Name));
                }
            }

            // ReSharper disable RedundantThisQualifier
            domainModel.Id = this.Id;
            //domainModel.ParentId = this.ParentId;
            // ReSharper restore RedundantThisQualifier
            
            return domainModel;
        } 
        #endregion

        #region Standard Properties for Form Models
        [ScaffoldColumn(false)]
        public long Id { get; set; }

        [ScaffoldColumn(false)]
        // ReSharper disable UnusedAutoPropertyAccessor.Local
        public virtual string Label { get; private set; }
        // ReSharper restore UnusedAutoPropertyAccessor.Local

        [ScaffoldColumn(false), NotReflectionMapped]
        public virtual string MyControllerName { get { return typeof (EntityT).Name; } }
        #endregion

        #region Validation
        //The default implemetation just grabs domain model validation but this can be overriden
        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            return CreateTempEntity().Validate(validationContext);
        }         
        #endregion
        
        #region Private Helper Methods
        private PropertyInfo GetMatchingDomainProperty(PropertyInfo myProperty, EntityT domainModel)
        {
            //Property Name is either name of the property or name supplied by ReflectionMappedToAttribute
            string myPropertyName;
            var myReflectionMappedToAttrubutes = myProperty.GetCustomAttributes(typeof(ReflectionMappedToAttribute), true);
            if (myReflectionMappedToAttrubutes.Count() == 0) myPropertyName = myProperty.Name;
            else myPropertyName = ((ReflectionMappedToAttribute)myReflectionMappedToAttrubutes.First()).PropertyName;

            //Check if Domain Model has an appropriate property, grab it if exists, otherwsie throw exception
            var domainModelProperties = domainModel.GetType().GetProperties().Where(x => x.Name == myPropertyName);
            // ReSharper disable PossibleMultipleEnumeration
            if (domainModelProperties.Count() != 1) throw new FormModelPropertyCantBeAutomapedException(string.Format("Property '{0}' of form model class '{1}' can't be automapped to domain model type '{2}' because '{3}' property does not exist in domain model.", myProperty.Name, GetType().Name, domainModel.GetType().Name, myPropertyName));
            return domainModelProperties.First();
            // ReSharper restore PossibleMultipleEnumeration
        }
        private EntityT CreateTempEntity()
        {
            return UpdateDomainModelEntity(new EntityT());
        }
        #endregion
    }
    
    abstract public class FormModelBase : IFormModelEditorTemplate
    {
        public virtual bool GetIEditorTemplateImplemented() { return true; }
        public virtual MvcHtmlString EditorTemplate(HtmlHelper html)
        {
            return html.FormModelEditor();
        }
    }
}
